#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <float.h>

#include "pub_lib.h"
#include "shader.h"

//#define DEBUG_VER

struct triangle {
	/* the original coordinates of the triangle, 
	 * v0, v1, v2 in counter clockwise order */
	Quaternion v[3]; //Vector4
	Vector3 color[3]; //color at each vertex
	Vector2 tex_coords[3]; //texture u,v
	Vector3 normal[3]; //normal vector for each vertex
};

/* global vars */
const int screenWidth = 700;
const int screenHeight = 700;

static struct rasterize_info raster_info;
static const char res_path[] = "./res/homework3/models/spot/";

/* initialize the rasterization */
void rasterize_init(struct rasterize_info *raster, int width, int height)
{
	raster->width = width;
	raster->height = height;
	int count;
	count = width * height;

	raster->frame_buf = calloc(count, sizeof(Color));
	raster->depth_buf = calloc(count, sizeof(float));

	int i;
	for( i = 0; i < count; i++)
		raster->depth_buf[i] = FLT_MAX;
}

void rasterize_exit(struct rasterize_info *raster)
{
	free(raster->depth_buf);
	free(raster->frame_buf);
}

//reset rasterization, prepare shader change
void rasterize_reset(struct rasterize_info *raster)
{
	int count;
	count = raster->width * raster->height;

	for(int i = 0; i < count; i++) {
		raster->frame_buf[i] = BLANK; //{0, 0, 0, 0}
		raster->depth_buf[i] = FLT_MAX;
	}
}

void print_help(void)
{
	printf("please select model: \n");
	printf("1: normal \n");
	printf("2: blinn-phong \n");
	printf("3: texture \n");
	printf("4: bump \n");
	printf("5: displacement \n");
}

void rasterize_change_shader(struct rasterize_info *raster, int  index)
{
	if(index < 1 || index > 5)
		return;
	rasterize_reset(raster);
	switch(index){
		case 1: //normal
			raster->fShader = normal_fragment_shader;
			break;
		case 2: //blinn-pong
			raster->fShader = phong_fragment_shader;
			break;
		case 3: //texture
			raster->fShader = texture_fragment_shader;
			raster->curImage = &raster->image;
			break;
		case 4: //bump
			raster->fShader = bump_fragment_shader;
			raster->curImage = &raster->image2;
			break;
		case 5:
			raster->fShader = disp_fragment_shader;
			raster->curImage = &raster->image2;
			break;
		default:
			break;
	}
}


/*
 * if point p(x, y) inside triangle(a, b, c)
 * */
bool insideTriangle(float x, float y, Vector3 a, Vector3 b, Vector3 c, Vector3* point)
{
	Vector3 p = {x, y, 1.0f};

	Vector3 AB = Vector3Subtract(b, a); //B - A;
    Vector3 BC = Vector3Subtract(c, b); //C - B;
    Vector3 CA = Vector3Subtract(a, c); //A - C;

    Vector3 AP = Vector3Subtract(p, a); //P - A;
    Vector3 BP = Vector3Subtract(p, b); //P - B;
    Vector3 CP = Vector3Subtract(p, c); //P - C;

	Vector3 result = Vector3CrossProduct(AB, AP);
	float z1 = result.z;

	result = Vector3CrossProduct(BC, BP);
	float z2 = result.z;

	result = Vector3CrossProduct(CA, CP);
	float z3 = result.z;

	*point = p;

    return (z1 > 0 && z2 > 0 && z3 > 0) || (z1 < 0 && z2 < 0 && z3 < 0);
}

/* interv at post-projection
 * now convert to pre-projection
 * return z value after interpolated */
float adjustZ(Quaternion* v, Vector3 interv)
{
	float z = 1.0f;
	// u/Za + v/Zb + w/Zc, (Zz, Zb, Zc) is depth value
	z = interv.x/v[0].z + interv.y/v[1].z + interv.z/v[2].z;
	z = fabsf(1.0f / z);
	return z;
}

/*
 * v0_attr, v1_attr, v3_attr: attribution of triangle 3 vertex
 * (interv.x, interv.y, interv.z) = (alpha, beta, gama) = (u,v,w)
 * val = z*(u*attr/Za + v*attr/Zb + w*attr/Zc)
 * return interpolate value
 * */
float adjustAttr(Quaternion* v, Vector3 interv, float z,
	float v0_attr, float v1_attr, float v2_attr)
{
	float value;
	value = interv.x * v0_attr / v[0].z +
			interv.y * v1_attr / v[1].z +
			interv.z * v2_attr / v[2].z;
	return (value * z);
}

Vector3 interpolate3(Quaternion* v, Vector3 interv, float z, Vector3* attr)
{
	float value[3];

	value[0] = adjustAttr(v, interv, z, attr[0].x, attr[1].x, attr[2].x);
	value[1] = adjustAttr(v, interv, z, attr[0].y, attr[1].y, attr[2].y);
	value[2] = adjustAttr(v, interv, z, attr[0].z, attr[1].z, attr[2].z);

	Vector3 result = {value[0], value[1], value[2]};
	return result;
}

Vector2 interpolate2(Quaternion* v, Vector3 interv, float z, Vector2* attr)
{
	float value[2];
	value[0] = adjustAttr(v, interv, z, attr[0].x, attr[1].x, attr[2].x);
	value[1] = adjustAttr(v, interv, z, attr[0].y, attr[1].y, attr[2].y);
	Vector2 result = {value[0], value[1]};
	return result;
}

/*
 * update the rasterization buffer by point p(x, y) in triangle(a, b, c)
 * please ensure p in triangle plane
 * 1. Interpolate the attributes of this triangle(ptri)
 * 2. shading
 * */
void fb_update(struct triangle *t, struct rasterize_info *raster,
	Vector3 p, int x, int y, Vector3 a, Vector3 b, Vector3 c, Vector3* view_pos)
{
	Vector3 interV;
	int index;
	float z_interpolated;

	/* If so, use the following code to get the interpolated z value
	 * Compute barycenter coordinates (u, v, w) for point p
	 * */
	interV = Vector3Barycenter(p, a, b, c);
	z_interpolated = adjustZ(t->v, interV);
	index = get_point_index(x, y, raster->width, raster->height);
	if (z_interpolated >= raster->depth_buf[index]) {
		return;
	}

	raster->depth_buf[index] = z_interpolated;
	// TODO: Interpolate the attributes:
	Vector3 color = interpolate3(t->v, interV, z_interpolated, t->color);
	Vector3 normal= interpolate3(t->v, interV, z_interpolated, t->normal);
	Vector2 tex   = interpolate2(t->v, interV, z_interpolated, t->tex_coords);
	Vector3 shade = interpolate3(t->v, interV, z_interpolated, view_pos);

	struct fragment_shader_payload payload;
	payload.raster = raster;
	payload.color = color;
	payload.normal = Vector3Normalize(normal);
	payload.tex_coords = tex;
	payload.view_pos = shade;
	payload.pImage = raster->curImage;

	raster->frame_buf[index] = raster->fShader(&payload);
	//DrawPixel(x, y, raster->frame_buf[index]);
}

//sample points
void rasterize_triangle(struct triangle *ptri, struct rasterize_info *raster, Vector3* view_pos)
{
	Vector3 a = {ptri->v[0].x, ptri->v[0].y, ptri->v[0].z};
	Vector3 b = {ptri->v[1].x, ptri->v[1].y, ptri->v[1].z};
	Vector3 c = {ptri->v[2].x, ptri->v[2].y, ptri->v[2].z};

	BoundingBox box;
	int x, y;
	Vector3 point;

	//Find out the bounding box of current triangle
	box = calcBoundingBox(a, b, c);
	int x_min = floorf(box.min.x);
	int x_max = ceilf(box.max.x);
	int y_min = floorf(box.min.y);
	int y_max = ceilf(box.max.y);
	// iterate through the pixel and find if the current pixel is inside the triangle
    for (x = x_min; x <= x_max; x++)
    {
        for (y = y_min; y <= y_max; y++)
        {
			if (!insideTriangle(x+0.5f, y+0.5f, a, b, c, &point)) {
				continue;
            }
			fb_update(ptri, raster, point, x, y, a, b, c, view_pos);
		}
	}
}

/* draw by point list */
void drawModel(struct rasterize_info *raster)
{
	int i, j, index;

	for(i = 0; i < raster->width; i++) {
		for ( j = 0; j < raster->height; j++) {
			index = get_point_index(i, j, raster->width, raster->height);
			if (sampled_point(raster->frame_buf[index])) {
				DrawPixel(i, j, raster->frame_buf[index]);
			}
		}
	}
}

/* initialize a triangle by raster->mesh */
void init_triangle(Mesh *mesh, struct triangle *pt, int index)
{
	float *v;
	int i;
	i = index * 9; //triangle has 3 vertices, every vertex has (x,y,z)
	//Vertex position(XYZ - 3 components per vertex)
	v = &(mesh->vertices[i]);
	init_triangle_points(v, (void*)pt->v, VERTEX4);

	//Vertex normals(XYZ - 3 components per vertex)
	v = &(mesh->normals[i]);
	init_triangle_points(v, (void*)pt->normal, VERTEX3);

	i = index * 6; // 3 vertices, (x, y)
	//Vertex texture coordinates (UV - 2 components per vertex)
	v = &(mesh->texcoords[i]);
	init_triangle_points(v, (void*)pt->tex_coords, VERTEX2);
}

//vertex transformation
void transform_vertexs(Quaternion* ori, Matrix m, Quaternion* newV, int width, int height)
{
	Quaternion point[3];
	int i;
	float w;
	Quaternion *p = point;
	for(i = 0; i < 3; i++, p++) {
		*p = QuaternionTransform(ori[i], m);
		//homogenous coordinates to orthonormal coordinates
		w = p->w;
		p->x /= w;
		p->y /= w;
		p->z /= w;
		p->w = 1.0f;

		//viewport transformation
		newV[i].x = 0.5f * width * (1.0f - p->x); //different from CV
		newV[i].y = 0.5f * height * (1.0f + p->y);
		newV[i].z = w;
	}
}

//Normal transformation
void transform_normals(Vector3* ori, Matrix m, Vector3* newV)
{
	Quaternion point[3];
	Quaternion val;
	Quaternion *p = point;

	int i;
	for(i = 0; i < 3; i++, p++) {
		to_vec4(*p, ori[i], 0.0f);
		val = QuaternionTransform(*p, m);
		QuadToV3(newV[i], val);
	}
}

void load_res(struct rasterize_info *raster)
{
	/* raster initialization */
	raster->model  = LoadModel(TextFormat("%s%s", res_path, "spot_triangulated_good.obj"));
	raster->image  = LoadImage(TextFormat("%s%s", res_path, "spot_texture.png"));
	raster->image2 = LoadImage(TextFormat("%s%s", res_path, "hmap.jpg"));

	raster->fShader = normal_fragment_shader;
	raster->curImage = &raster->image2;
}

void unload_res(struct rasterize_info *raster)
{
	UnloadModel(raster->model);
	UnloadImage(raster->image);
	UnloadImage(raster->image2);
}

/* shading
 * */
void shading_object(struct rasterize_info *raster, float angle, Quaternion eye)
{
	/* Matrix transformation */
	Matrix model, view, projection;
	Matrix pv, vm, mvp;

	//calc transformation M, V, P
	model = get_model_matrix(angle);
	view = get_view_matrix(eye);
	projection = get_projection_matrix(45, 1, -0.1f, -50.0f);

	//calc MVP: projection - view - Module
	pv = matrxiMul(projection, view); //pv = P * V
	mvp = matrxiMul(pv, model); //MVP = PV * M = P * V * M
	vm = matrxiMul(view, model); //vm = v * m

	//(view * model).inverse().transpose();
	Matrix invert = MatrixInvert(vm);
	Matrix inv_trans = MatrixTranspose(invert);

	struct triangle t, newt;
	int index; //triangle count
	Vector3 viewspace_pos[3];

	Vector3 cols = {148, 121, 92};
	cols = Vector3Scale(cols, 1.0f/255.0f);
	Mesh *mesh = raster->model.meshes;

	//traverse all triangles defined by OBJ file
	for( index = 0; index < mesh->triangleCount; index++) {
		init_triangle(mesh, &t, index);
		newt = t;
		//viewspace_pos = t.vertex[3] transform by matrix vm
		trans_point(viewspace_pos, t.v, vm);
		//transform vetex
		transform_vertexs(t.v, mvp, newt.v, raster->width, raster->height);
		//transform normal
		transform_normals(t.normal, inv_trans, newt.normal);
		newt.color[0] = cols;
		newt.color[1] = cols;
		newt.color[2] = cols;
		rasterize_triangle(&newt, raster, viewspace_pos);
	}
}

int main(int argc, char** argv)
{
	// Initialization
	char key;
	char* str = "please input 1 - 5";
	int index = -1, last_index = -1;
	float angle = 140.0;
	Quaternion eye_pos = {0, 0, 10, 1};
	//--------------------------------------------------------------------------------------
	rasterize_init(&raster_info, screenWidth, screenHeight);

	InitWindow(screenWidth, screenHeight, "homework3 - shading");
	SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
	//--------------------------------------------------------------------------------------
	print_help();
	//LoadModel and LoadTexture should be after InitWindow() call
	load_res(&raster_info);

	// Main game loop
	while (!WindowShouldClose())    // Detect window close button or ESC key
	{
		// Update
		//----------------------------------------------------------------------------------
		// TODO: Update your variables here
		key = (char)GetKeyPressed();
		if (key == ESC_KEY) {
			break;
		} else if (key == '1') {
			index = 1;
			str = "normal";
		} else if(key == '2') {
			index = 2;
			str = "blinn-Phong";
		}else if(key == '3') {
			index = 3;
			str = "texture";
		}else if(key == '4') {
			index = 4;
			str = "bump";
		}else if(key == '5') {
			index = 5;
			str = "displacement";
		}

		//----------------------------------------------------------------------------------
		// Draw
		//----------------------------------------------------------------------------------
		BeginDrawing();

#ifdef DEBUG_VER
		Vector3 eye;
		QuadToV3(eye, eye_pos);
		Vector3 axis = {0, 1, 0};
		draw_modelEx(&raster_info.model, eye, 45.0f, 3.0f, axis, angle);
#else
		ClearBackground(BLACK);
		DrawText(TextFormat("shading method: %s", str), 20, 20, 20, WHITE);
		if (index != -1)
			drawModel(&raster_info);

		if(index != last_index) {
			last_index = index;
			printf("shading changed to %s\n", str);
			rasterize_change_shader(&raster_info, index);
			shading_object(&raster_info, angle, eye_pos);
		}
#endif

		EndDrawing();
		//----------------------------------------------------------------------------------
	}

	// De-Initialization
	unload_res(&raster_info);

	//--------------------------------------------------------------------------------------
	CloseWindow();        // Close window and OpenGL context
	//--------------------------------------------------------------------------------------

	rasterize_exit(&raster_info);
	return 0;
}
